import { IncomingMessage } from 'http'
import { parse } from 'url'
import { WebSocket } from 'ws'
import i18n from '@/i18n'
import {
  ConnectedSocket,
  MessageBody,
  OnGatewayConnection,
  OnGatewayDisconnect,
  OnGatewayInit,
  SubscribeMessage,
} from '@nestjs/websockets'
import { Assist } from '../assist/assist'
import { AssistException } from '../assist/assist-exception'
import { AppLogger } from '../logger/app-logger'
import { ExpireStore } from '../store/expire-store'
import { ActionSocketPingDto } from './dto/action-socket-ping.dto'
import { ActionSocketSendDto } from './dto/action-socket-send.dto'
import { ActionSocketAction } from './entity/action-socket-action'
import { ActionSocketCode } from './entity/action-socket-code'
import { ActionSocketMessage } from './entity/action-socket-message'
import { ActionSocketUrlParam } from './entity/action-socket-url-param'

export class ActionSocketService<
    P extends ActionSocketUrlParam = ActionSocketUrlParam,
    A extends string = string,
    C extends string = string,
  >
  implements OnGatewayConnection<WebSocket>, OnGatewayDisconnect<WebSocket>, OnGatewayInit
{
  protected readonly logger = new AppLogger(ActionSocketService.name)

  /**
   * 每个client都有一个唯一标识号
   * uniqueId - WebSocket
   */
  protected readonly clientMap = new Map<string, WebSocket>()

  /**
   * 根据userToken映射client
   * userToken - Websocket[]
   */
  protected readonly userTokenClientMap = new Map<string, WebSocket[]>()

  /**
   * 由于client.url是undefined，故而缓存client - 数据关系
   * Websocket - { url: string; connectTime: number }
   */
  protected readonly clientDataWeakMap = new WeakMap<WebSocket, { url: string; connectTime: number }>()

  /**
   * 活动中的客户端缓存，指定时间后自动失效
   */
  protected readonly clientExpireStore = new ExpireStore<WebSocket>({
    expire: 10 * 60 * 1000,
    onExpire: client => {
      const data = this.clientDataWeakMap.get(client)
      if (data) {
        this.removeClient(client)
        this.logger.info(() =>
          i18n.t('lhy-common.ws.action-socket.12153265-0', {
            args: [this.clientMap.size, data.url, Date.now() - data.connectTime],
          })
        )
      } else {
        client.close(4000, i18n.t('lhy-common.ws.action-socket.12153265-1'))
      }
    },
  })

  constructor(protected readonly config: ActionSocketServiceConfig<P>) {}

  afterInit(server: any) {
    this.logger.info(() => i18n.t('lhy-common.ws.action-socket.12153265-2'))
  }

  /**
   * 鉴权，鉴权失败时自动断开连接
   * @param param
   * @param url
   * @param client
   * @param onSuccess
   */
  async auth(param: P, url: string, client: WebSocket, onSuccess?: () => void) {
    try {
      // 鉴权
      await this.config.auth(param as unknown as P, client)
      this.logger.info(i18n.t('lhy-common.ws.action-socket.12153265-3', { args: [url] }))

      onSuccess?.()
    } catch (e) {
      await this.sendMessage(client, {
        action: ActionSocketAction.Prompt as A,
        code: ActionSocketCode.InvalidUserToken as C,
        data: i18n.t('lhy-common.ws.action-socket.12153265-4') + (e.message || e),
      })
      client.close()
      this.logger.error(
        i18n.t('lhy-common.ws.action-socket.12153265-5') +
          (e.message || e) +
          i18n.t('lhy-common.ws.action-socket.12153265-6', { args: [url] })
      )
    }
  }

  /**
   * 客户端连接时
   * @param client
   * @param incomingMessage
   */
  async handleConnection(client: WebSocket, incomingMessage: IncomingMessage) {
    const url = client.url || incomingMessage.url

    if (this.config.authOnConnected) {
      this.auth(this.getClientParam(url), url, client, () => {
        this.addClient(client, url)
        this.logger.info(() => i18n.t('lhy-common.ws.action-socket.12153265-7', { args: [this.clientMap.size, url] }))
      })
    } else {
      this.addClient(client, url)
      this.logger.info(() => i18n.t('lhy-common.ws.action-socket.12153265-7', { args: [this.clientMap.size, url] }))
    }
  }

  /**
   * 客户端断开时
   * @param client
   */
  handleDisconnect(client: WebSocket) {
    const data = this.clientDataWeakMap.get(client)
    this.removeClient(client)
    data &&
      this.logger.info(() =>
        i18n.t('lhy-common.ws.action-socket.12153265-8', {
          args: [this.clientMap.size, data.url, Date.now() - data.connectTime],
        })
      )
  }

  /**
   * 接收ping时
   * @param client
   * @param data
   */
  @SubscribeMessage('ping')
  handlePing(@ConnectedSocket() client: WebSocket, @MessageBody() data: ActionSocketPingDto) {
    this.logger.debug(() => `receive ping [url=${this.clientDataWeakMap.get(client)?.url}]: ` + JSON.stringify(data))

    this.clientExpireStore.update(client)
    this.sendMessage<ActionSocketPingDto>(client, {
      action: ActionSocketAction.Pong as A,
      data: { time: Date.now() },
    })
  }

  /**
   * 发送消息
   * @param param
   * @returns
   */
  send<T>(param: ActionSocketSendDto<T, A, C>) {
    return new Promise<void>((resolve, reject) => {
      try {
        Assist.threw(!param.uniqueId && !param.userToken, i18n.t('lhy-common.ws.action-socket.12153265-9'))

        if (param.uniqueId) {
          const client = this.clientMap.get(param.uniqueId)
          Assist.threw(
            !client,
            new AssistException(
              i18n.t('lhy-common.ws.action-socket.12153265-10', { args: [param.uniqueId] }),
              ActionSocketCode.UniqueIdExpired
            )
          )

          return this.sendMessage(client, param).then(resolve, reject)
        }

        if (param.userToken) {
          const clients = this.userTokenClientMap.get(param.userToken)

          return Promise.all(clients.map(client => this.sendMessage(client, param))).then(() => resolve(), reject)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  /**
   * 发送消息
   * @param client
   * @param message
   * @returns
   */
  protected sendMessage<T>(client: WebSocket, message: ActionSocketMessage<T, A, C>) {
    return new Promise<void>((resolve, reject) => {
      return client.send(
        JSON.stringify({
          action: message.action,
          code: message.code,
          data: typeof message.data === 'function' ? (message.data as Function)() : message.data,
        }),
        err => {
          if (err) {
            reject(err)
          } else {
            resolve()
          }
        }
      )
    })
  }

  /**
   * 从url中获取参数
   * @param url
   * @returns
   */
  protected getClientParam(url: string) {
    const parsedUrl = parse(url, true)
    const platform = parsedUrl.query.platform as string
    const userToken = parsedUrl.query.userToken as string
    const uniqueId = parsedUrl.query.uniqueId as string

    Assist.threw(!platform, i18n.t('lhy-common.ws.action-socket.12153265-11'))
    Assist.threw(!userToken, i18n.t('lhy-common.ws.action-socket.12153265-12'))
    Assist.threw(!uniqueId, i18n.t('lhy-common.ws.action-socket.12153265-13'))

    return parsedUrl.query as unknown as P
  }

  /**
   * 添加客户端
   * @param client
   * @param url
   */
  protected addClient(client: WebSocket, url: string) {
    const { userToken, uniqueId } = this.getClientParam(url)
    this.clientMap.get(uniqueId)?.close()
    this.clientMap.set(uniqueId, client)

    const userTokenClients = this.userTokenClientMap.get(userToken) || []
    if (!userTokenClients.includes(client)) {
      userTokenClients.push(client)
    }
    this.userTokenClientMap.set(userToken, userTokenClients)

    this.clientDataWeakMap.set(client, { url, connectTime: Date.now() })
    this.clientExpireStore.update(client)
  }

  /**
   * 删除客户端
   * @param platform
   * @param uniqueId
   * @returns
   */
  protected removeClient(client: WebSocket) {
    client.close()
    this.clientExpireStore.remove(client)

    const data = this.clientDataWeakMap.get(client)
    if (!data) {
      return
    }
    this.clientDataWeakMap.delete(client)

    const { userToken, uniqueId } = this.getClientParam(data.url)
    this.clientMap.delete(uniqueId)

    if (!this.userTokenClientMap.has(userToken)) {
      return
    }

    const userTokenClients = this.userTokenClientMap.get(userToken)
    const index = userTokenClients.indexOf(client)
    if (index > -1) {
      userTokenClients.splice(index, 1)
      if (!userTokenClients.length) {
        this.userTokenClientMap.delete(userToken)
      }
    }
  }
}

export interface ActionSocketServiceConfig<P extends ActionSocketUrlParam = ActionSocketUrlParam> {
  /**
   * 鉴权
   * @param param
   * @param client
   * @returns
   */
  auth: (param: P, client: WebSocket) => Promise<void>
  /**
   * 连接创建成功时进行鉴权，默认：false
   */
  authOnConnected?: boolean
}
